import { CommandUtils } from "./CommandUtils"
import * as path from "path"
import * as yargs from "yargs"
import chalk from "chalk"
import { exec } from "child_process"
import { TypeORMError } from "../error"
import { PlatformTools } from "../platform/PlatformTools"

/**
 * Generates a new project with TypeORM.
 */
export class InitCommand implements yargs.CommandModule {
    command = "init"
    describe =
        "Generates initial TypeORM project structure. " +
        "If name specified then creates files inside directory called as name. " +
        "If its not specified then creates files inside current directory."

    builder(args: yargs.Argv) {
        return args
            .option("n", {
                alias: "name",
                describe: "Name of the project directory.",
            })
            .option("db", {
                alias: "database",
                describe: "Database type you'll use in your project.",
            })
            .option("express", {
                describe:
                    "Indicates if express server sample code should be included in the project. False by default.",
            })
            .option("docker", {
                describe:
                    "Set to true if docker-compose must be generated as well. False by default.",
            })
            .option("pm", {
                alias: "manager",
                choices: ["npm", "yarn"],
                default: "npm",
                describe: "Install packages, expected values are npm or yarn.",
            })
            .option("ms", {
                alias: "module",
                choices: ["commonjs", "esm"],
                default: "commonjs",
                describe:
                    "Module system to use for project, expected values are commonjs or esm.",
            })
    }

    async handler(args: yargs.Arguments) {
        try {
            const database: string = (args.database as any) || "postgres"
            const isExpress = args.express !== undefined ? true : false
            const isDocker = args.docker !== undefined ? true : false
            const basePath = process.cwd() + (args.name ? "/" + args.name : "")
            const projectName = args.name
                ? path.basename(args.name as any)
                : undefined
            const installNpm = args.pm === "yarn" ? false : true
            const projectIsEsm = args.ms === "esm"
            await CommandUtils.createFile(
                basePath + "/package.json",
                InitCommand.getPackageJsonTemplate(projectName, projectIsEsm),
                false,
            )
            if (isDocker)
                await CommandUtils.createFile(
                    basePath + "/docker-compose.yml",
                    InitCommand.getDockerComposeTemplate(database),
                    false,
                )
            await CommandUtils.createFile(
                basePath + "/.gitignore",
                InitCommand.getGitIgnoreFile(),
            )
            await CommandUtils.createFile(
                basePath + "/README.md",
                InitCommand.getReadmeTemplate({ docker: isDocker }),
                false,
            )
            await CommandUtils.createFile(
                basePath + "/tsconfig.json",
                InitCommand.getTsConfigTemplate(projectIsEsm),
            )
            await CommandUtils.createFile(
                basePath + "/src/entity/User.ts",
                InitCommand.getUserEntityTemplate(database),
            )
            await CommandUtils.createFile(
                basePath + "/src/data-source.ts",
                InitCommand.getAppDataSourceTemplate(projectIsEsm, database),
            )
            await CommandUtils.createFile(
                basePath + "/src/index.ts",
                InitCommand.getAppIndexTemplate(isExpress, projectIsEsm),
            )
            await CommandUtils.createDirectories(basePath + "/src/migration")

            // generate extra files for express application
            if (isExpress) {
                await CommandUtils.createFile(
                    basePath + "/src/routes.ts",
                    InitCommand.getRoutesTemplate(projectIsEsm),
                )
                await CommandUtils.createFile(
                    basePath + "/src/controller/UserController.ts",
                    InitCommand.getControllerTemplate(projectIsEsm),
                )
            }

            const packageJsonContents = await CommandUtils.readFile(
                basePath + "/package.json",
            )
            await CommandUtils.createFile(
                basePath + "/package.json",
                InitCommand.appendPackageJson(
                    packageJsonContents,
                    database,
                    isExpress,
                    projectIsEsm,
                ),
            )

            if (args.name) {
                console.log(
                    chalk.green(
                        `Project created inside ${chalk.blue(
                            basePath,
                        )} directory.`,
                    ),
                )
            } else {
                console.log(
                    chalk.green(`Project created inside current directory.`),
                )
            }

            console.log(chalk.green(`Please wait, installing dependencies...`))
            if (args.pm && installNpm) {
                await InitCommand.executeCommand("npm install", basePath)
            } else {
                await InitCommand.executeCommand("yarn install", basePath)
            }

            console.log(chalk.green(`Done! Start playing with a new project!`))
        } catch (err) {
            PlatformTools.logCmdErr("Error during project initialization:", err)
            process.exit(1)
        }
    }

    // -------------------------------------------------------------------------
    // Protected Static Methods
    // -------------------------------------------------------------------------

    protected static executeCommand(command: string, cwd: string) {
        return new Promise<string>((ok, fail) => {
            exec(command, { cwd }, (error: any, stdout: any, stderr: any) => {
                if (stdout) return ok(stdout)
                if (stderr) return fail(stderr)
                if (error) return fail(error)
                ok("")
            })
        })
    }

    /**
     * Gets contents of the ormconfig file.
     */
    protected static getAppDataSourceTemplate(
        isEsm: boolean,
        database: string,
    ): string {
        let dbSettings = ""
        switch (database) {
            case "mysql":
                dbSettings = `type: "mysql",
    host: "localhost",
    port: 3306,
    username: "test",
    password: "test",
    database: "test",`
                break
            case "mariadb":
                dbSettings = `type: "mariadb",
    host: "localhost",
    port: 3306,
    username: "test",
    password: "test",
    database: "test",`
                break
            case "sqlite":
                dbSettings = `type: "sqlite",
    database: "database.sqlite",`
                break
            case "better-sqlite3":
                dbSettings = `type: "better-sqlite3",
    database: "database.sqlite",`
                break
            case "postgres":
                dbSettings = `type: "postgres",
    host: "localhost",
    port: 5432,
    username: "test",
    password: "test",
    database: "test",`
                break
            case "cockroachdb":
                dbSettings = `type: "cockroachdb",
    host: "localhost",
    port: 26257,
    username: "root",
    password: "",
    database: "defaultdb",`
                break
            case "mssql":
                dbSettings = `type: "mssql",
    host: "localhost",
    username: "sa",
    password: "Admin12345",
    database: "tempdb",`
                break
            case "oracle":
                dbSettings = `type: "oracle",
host: "localhost",
username: "system",
password: "oracle",
port: 1521,
sid: "xe.oracle.docker",`
                break
            case "mongodb":
                dbSettings = `type: "mongodb",
    database: "test",`
                break
            case "spanner":
                dbSettings = `type: "spanner",
    projectId: "test",
    instanceId: "test",
    databaseId: "test",`
                break
        }
        return `import "reflect-metadata"
import { DataSource } from "typeorm"
import { User } from "./entity/User${isEsm ? ".js" : ""}"

export const AppDataSource = new DataSource({
    ${dbSettings}
    synchronize: true,
    logging: false,
    entities: [User],
    migrations: [],
    subscribers: [],
})
`
    }

    /**
     * Gets contents of the ormconfig file.
     */
    protected static getTsConfigTemplate(esmModule: boolean): string {
        if (esmModule)
            return JSON.stringify(
                {
                    compilerOptions: {
                        lib: ["es2021"],
                        target: "es2021",
                        module: "es2022",
                        moduleResolution: "node",
                        allowSyntheticDefaultImports: true,
                        outDir: "./build",
                        emitDecoratorMetadata: true,
                        experimentalDecorators: true,
                        sourceMap: true,
                    },
                },
                undefined,
                3,
            )
        else
            return JSON.stringify(
                {
                    compilerOptions: {
                        lib: ["es5", "es6"],
                        target: "es5",
                        module: "commonjs",
                        moduleResolution: "node",
                        outDir: "./build",
                        emitDecoratorMetadata: true,
                        experimentalDecorators: true,
                        sourceMap: true,
                    },
                },
                undefined,
                3,
            )
    }

    /**
     * Gets contents of the .gitignore file.
     */
    protected static getGitIgnoreFile(): string {
        return `.idea/
.vscode/
node_modules/
build/
tmp/
temp/`
    }

    /**
     * Gets contents of the user entity.
     */
    protected static getUserEntityTemplate(database: string): string {
        return `import { Entity, ${
            database === "mongodb"
                ? "ObjectIdColumn, ObjectId"
                : "PrimaryGeneratedColumn"
        }, Column } from "typeorm"

@Entity()
export class User {

    ${
        database === "mongodb"
            ? "@ObjectIdColumn()"
            : "@PrimaryGeneratedColumn()"
    }
    id: ${database === "mongodb" ? "ObjectId" : "number"}

    @Column()
    firstName: string

    @Column()
    lastName: string

    @Column()
    age: number

}
`
    }

    /**
     * Gets contents of the route file (used when express is enabled).
     */
    protected static getRoutesTemplate(isEsm: boolean): string {
        return `import { UserController } from "./controller/UserController${
            isEsm ? ".js" : ""
        }"

export const Routes = [{
    method: "get",
    route: "/users",
    controller: UserController,
    action: "all"
}, {
    method: "get",
    route: "/users/:id",
    controller: UserController,
    action: "one"
}, {
    method: "post",
    route: "/users",
    controller: UserController,
    action: "save"
}, {
    method: "delete",
    route: "/users/:id",
    controller: UserController,
    action: "remove"
}]`
    }

    /**
     * Gets contents of the user controller file (used when express is enabled).
     */
    protected static getControllerTemplate(isEsm: boolean): string {
        return `import { AppDataSource } from "../data-source${
            isEsm ? ".js" : ""
        }"
import { NextFunction, Request, Response } from "express"
import { User } from "../entity/User${isEsm ? ".js" : ""}"

export class UserController {

    private userRepository = AppDataSource.getRepository(User)

    async all(request: Request, response: Response, next: NextFunction) {
        return this.userRepository.find()
    }

    async one(request: Request, response: Response, next: NextFunction) {
        const id = parseInt(request.params.id)


        const user = await this.userRepository.findOne({
            where: { id }
        })

        if (!user) {
            return "unregistered user"
        }
        return user
    }

    async save(request: Request, response: Response, next: NextFunction) {
        const { firstName, lastName, age } = request.body;

        const user = Object.assign(new User(), {
            firstName,
            lastName,
            age
        })

        return this.userRepository.save(user)
    }

    async remove(request: Request, response: Response, next: NextFunction) {
        const id = parseInt(request.params.id)

        let userToRemove = await this.userRepository.findOneBy({ id })

        if (!userToRemove) {
            return "this user not exist"
        }

        await this.userRepository.remove(userToRemove)

        return "user has been removed"
    }

}`
    }

    /**
     * Gets contents of the main (index) application file.
     */
    protected static getAppIndexTemplate(
        express: boolean,
        isEsm: boolean,
    ): string {
        if (express) {
            return `import ${!isEsm ? "* as " : ""}express from "express"
import ${!isEsm ? "* as " : ""}bodyParser from "body-parser"
import { Request, Response } from "express"
import { AppDataSource } from "./data-source${isEsm ? ".js" : ""}"
import { Routes } from "./routes${isEsm ? ".js" : ""}"
import { User } from "./entity/User${isEsm ? ".js" : ""}"

AppDataSource.initialize().then(async () => {

    // create express app
    const app = express()
    app.use(bodyParser.json())

    // register express routes from defined application routes
    Routes.forEach(route => {
        (app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next)
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : undefined)

            } else if (result !== null && result !== undefined) {
                res.json(result)
            }
        })
    })

    // setup express app here
    // ...

    // start express server
    app.listen(3000)

    // insert new users for test
    await AppDataSource.manager.save(
        AppDataSource.manager.create(User, {
            firstName: "Timber",
            lastName: "Saw",
            age: 27
        })
    )

    await AppDataSource.manager.save(
        AppDataSource.manager.create(User, {
            firstName: "Phantom",
            lastName: "Assassin",
            age: 24
        })
    )

    console.log("Express server has started on port 3000. Open http://localhost:3000/users to see results")

}).catch(error => console.log(error))
`
        } else {
            return `import { AppDataSource } from "./data-source${
                isEsm ? ".js" : ""
            }"
import { User } from "./entity/User${isEsm ? ".js" : ""}"

AppDataSource.initialize().then(async () => {

    console.log("Inserting a new user into the database...")
    const user = new User()
    user.firstName = "Timber"
    user.lastName = "Saw"
    user.age = 25
    await AppDataSource.manager.save(user)
    console.log("Saved a new user with id: " + user.id)

    console.log("Loading users from the database...")
    const users = await AppDataSource.manager.find(User)
    console.log("Loaded users: ", users)

    console.log("Here you can setup and run express / fastify / any other framework.")

}).catch(error => console.log(error))
`
        }
    }

    /**
     * Gets contents of the new package.json file.
     */
    protected static getPackageJsonTemplate(
        projectName?: string,
        projectIsEsm?: boolean,
    ): string {
        return JSON.stringify(
            {
                name: projectName || "typeorm-sample",
                version: "0.0.1",
                description: "Awesome project developed with TypeORM.",
                type: projectIsEsm ? "module" : "commonjs",
                devDependencies: {},
                dependencies: {},
                scripts: {},
            },
            undefined,
            3,
        )
    }

    /**
     * Gets contents of the new docker-compose.yml file.
     */
    protected static getDockerComposeTemplate(database: string): string {
        switch (database) {
            case "mysql":
                return `version: '3'
services:

  mysql:
    image: "mysql:8.0.30"
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: "admin"
      MYSQL_USER: "test"
      MYSQL_PASSWORD: "test"
      MYSQL_DATABASE: "test"

`
            case "mariadb":
                return `version: '3'
services:

  mariadb:
    image: "mariadb:10.8.4"
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: "admin"
      MYSQL_USER: "test"
      MYSQL_PASSWORD: "test"
      MYSQL_DATABASE: "test"

`
            case "postgres":
                return `version: '3'
services:

  postgres:
    image: "postgres:14.5"
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: "test"
      POSTGRES_PASSWORD: "test"
      POSTGRES_DB: "test"

`
            case "cockroachdb":
                return `version: '3'
services:

  cockroachdb:
    image: "cockroachdb/cockroach:v22.1.6"
    command: start --insecure
    ports:
      - "26257:26257"

`
            case "sqlite":
            case "better-sqlite3":
                return `version: '3'
services:
`
            case "oracle":
                throw new TypeORMError(
                    `You cannot initialize a project with docker for Oracle driver yet.`,
                ) // todo: implement for oracle as well

            case "mssql":
                return `version: '3'
services:

  mssql:
    image: "microsoft/mssql-server-linux:rc2"
    ports:
      - "1433:1433"
    environment:
      SA_PASSWORD: "Admin12345"
      ACCEPT_EULA: "Y"

`
            case "mongodb":
                return `version: '3'
services:

  mongodb:
    image: "mongo:5.0.12"
    container_name: "typeorm-mongodb"
    ports:
      - "27017:27017"

`
            case "spanner":
                return `version: '3'
services:

  spanner:
    image: gcr.io/cloud-spanner-emulator/emulator:1.4.1
    ports:
      - "9010:9010"
      - "9020:9020"

`
        }
        return ""
    }

    /**
     * Gets contents of the new readme.md file.
     */
    protected static getReadmeTemplate(options: { docker: boolean }): string {
        let template = `# Awesome Project Build with TypeORM

Steps to run this project:

1. Run \`npm i\` command
`

        if (options.docker) {
            template += `2. Run \`docker-compose up\` command
`
        } else {
            template += `2. Setup database settings inside \`data-source.ts\` file
`
        }

        template += `3. Run \`npm start\` command
`
        return template
    }

    /**
     * Appends to a given package.json template everything needed.
     */
    protected static appendPackageJson(
        packageJsonContents: string,
        database: string,
        express: boolean,
        projectIsEsm: boolean /*, docker: boolean*/,
    ): string {
        const packageJson = JSON.parse(packageJsonContents)

        if (!packageJson.devDependencies) packageJson.devDependencies = {}
        Object.assign(packageJson.devDependencies, {
            "ts-node": "10.9.1",
            "@types/node": "^16.11.10",
            typescript: "4.5.2",
        })

        if (!packageJson.dependencies) packageJson.dependencies = {}
        Object.assign(packageJson.dependencies, {
            typeorm:
                require("../package.json").version !== "0.0.0"
                    ? require("../package.json").version // install version from package.json if present
                    : require("../package.json").installFrom, // else use custom source
            "reflect-metadata": "^0.1.13",
        })

        switch (database) {
            case "mysql":
            case "mariadb":
                packageJson.dependencies["mysql"] = "^2.14.1"
                break
            case "postgres":
            case "cockroachdb":
                packageJson.dependencies["pg"] = "^8.4.0"
                break
            case "sqlite":
                packageJson.dependencies["sqlite3"] = "^5.0.2"
                break
            case "better-sqlite3":
                packageJson.dependencies["better-sqlite3"] = "^7.0.0"
                break
            case "oracle":
                packageJson.dependencies["oracledb"] = "^5.1.0"
                break
            case "mssql":
                packageJson.dependencies["mssql"] = "^9.1.1"
                break
            case "mongodb":
                packageJson.dependencies["mongodb"] = "^5.2.0"
                break
            case "spanner":
                packageJson.dependencies["@google-cloud/spanner"] = "^5.18.0"
                break
        }

        if (express) {
            packageJson.dependencies["express"] = "^4.17.2"
            packageJson.dependencies["body-parser"] = "^1.19.1"
        }

        if (!packageJson.scripts) packageJson.scripts = {}

        if (projectIsEsm)
            Object.assign(packageJson.scripts, {
                start: /*(docker ? "docker-compose up && " : "") + */ "node --loader ts-node/esm src/index.ts",
                typeorm: "typeorm-ts-node-esm",
            })
        else
            Object.assign(packageJson.scripts, {
                start: /*(docker ? "docker-compose up && " : "") + */ "ts-node src/index.ts",
                typeorm: "typeorm-ts-node-commonjs",
            })

        return JSON.stringify(packageJson, undefined, 3)
    }
}
